package grdnlog.dao;

import grdnlog.bean.AppEngineConnectionData;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.persistence.EntityExistsException;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionKey;
import org.springframework.social.connect.ConnectionRepository;
import org.springframework.social.connect.DuplicateConnectionException;
import org.springframework.social.connect.NoSuchConnectionException;
import org.springframework.social.connect.NotConnectedException;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

public class GoogleAppEngineConnectionRepository implements ConnectionRepository {

	private final String userId;
	
	private final ConnectionFactoryLocator connectionFactoryLocator;

	private final TextEncryptor textEncryptor;
	
	public GoogleAppEngineConnectionRepository(String userId, ConnectionFactoryLocator connectionFactoryLocator, TextEncryptor textEncryptor) {
		this.userId = userId;
		this.connectionFactoryLocator = connectionFactoryLocator;
		this.textEncryptor = textEncryptor;
	}
	
	public Connection<?> mapConnectionData(ConnectionData connectionData) {
		ConnectionFactory<?> connectionFactory = connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId());
		return connectionFactory.createConnection(connectionData);
	}
	
	
	
	
	public MultiValueMap<String, Connection<?>> findAllConnections() {
		EntityManager em = this.getEntityManager();
		
		Query query = em.createQuery("SELECT o FROM grdnlog.bean.AppEngineConnectionData WHERE o.userId = ?1 ORDER BY o.providerId, o.rank");
		query.setParameter(1, userId);
		
		List<AppEngineConnectionData> queryResultList = query.getResultList();
		List<Connection<?>> resultList = new ArrayList<Connection<?>>();
		
		for (AppEngineConnectionData data : queryResultList) {
			resultList.add(mapConnectionData(data.getConnectionData()));
		}
		
		MultiValueMap<String, Connection<?>> connections = new LinkedMultiValueMap<String, Connection<?>>();
		Set<String> registeredProviderIds = connectionFactoryLocator.registeredProviderIds();
		for (String registeredProviderId : registeredProviderIds) {
			connections.put(registeredProviderId, Collections.<Connection<?>>emptyList());
		}
		for (Connection<?> connection : resultList) {
			String providerId = connection.getKey().getProviderId();
			if (connections.get(providerId).size() == 0) {
				connections.put(providerId, new LinkedList<Connection<?>>());
			}
			connections.add(providerId, connection);
		}
		return connections;
	}

	public List<Connection<?>> findConnections(String providerId) {
		EntityManager em = this.getEntityManager();
		
		Query query = em.createQuery("SELECT o FROM grdnlog.bean.AppEngineConnectionData WHERE o.userId = ?1 AND o.providerId = ?2 ORDER BY o.rank");
		query.setParameter(1, userId);
		query.setParameter(2, providerId);
		
		List<AppEngineConnectionData> queryResultList = query.getResultList();
		List<Connection<?>> resultList = new ArrayList<Connection<?>>();
		
		for (AppEngineConnectionData data : queryResultList) {
			resultList.add(mapConnectionData(data.getConnectionData()));
		}
		
		return resultList;
	}

	@SuppressWarnings("unchecked")
	public <A> List<Connection<A>> findConnections(Class<A> apiType) {
		List<?> connections = findConnections(getProviderId(apiType));
		return (List<Connection<A>>) connections;
	}
	
	public MultiValueMap<String, Connection<?>> findConnectionsToUsers(MultiValueMap<String, String> providerUsers) {
		if (providerUsers.isEmpty()) {
			throw new IllegalArgumentException("Unable to execute find: no providerUsers provided");
		}
		StringBuilder providerUsersCriteriaSql = new StringBuilder();
		Map<String,Object> parameters = new LinkedHashMap<String, Object>();
		parameters.put("userId", userId);
		for (Iterator<Entry<String, List<String>>> it = providerUsers.entrySet().iterator(); it.hasNext();) {
			Entry<String, List<String>> entry = it.next();
			String providerId = entry.getKey();
			providerUsersCriteriaSql.append("o.providerId = :providerId_").append(providerId).append(" AND o.providerUserId in (:providerUserIds_").append(providerId).append(")");
			parameters.put("providerId_" + providerId, providerId);
			
			// setting a list as a param is probably going to balk
			parameters.put("providerUserIds_" + providerId, entry.getValue());
			if (it.hasNext()) {
				providerUsersCriteriaSql.append(" OR " );
			}
		}
		
		EntityManager em = this.getEntityManager();
		Query query = em.createQuery("SELECT o FROM grdnlog.bean.AppEngineConnectionData WHERE o.userId = :userId AND " + providerUsersCriteriaSql + " ORDER BY o.providerId, o.rank");
		
		for(String key : parameters.keySet()) {
			query.setParameter(key, parameters.get(key));
		}
		
		List<AppEngineConnectionData> queryResultList = query.getResultList();
		List<Connection<?>> resultList = new ArrayList<Connection<?>>();
		
		for (AppEngineConnectionData data : queryResultList) {
			resultList.add(mapConnectionData(data.getConnectionData()));
		}
		
		//List<Connection<?>> resultList = new NamedParameterJdbcTemplate(jdbcTemplate).query(selectFromUserConnection() + " where userId = :userId and " + providerUsersCriteriaSql + " order by providerId, rank", parameters, connectionMapper);
		MultiValueMap<String, Connection<?>> connectionsForUsers = new LinkedMultiValueMap<String, Connection<?>>();
		for (Connection<?> connection : resultList) {
			String providerId = connection.getKey().getProviderId();
			List<String> userIds = providerUsers.get(providerId);
			List<Connection<?>> connections = connectionsForUsers.get(providerId);
			if (connections == null) {
				connections = new ArrayList<Connection<?>>(userIds.size());
				for (int i = 0; i < userIds.size(); i++) {
					connections.add(null);
				}
				connectionsForUsers.put(providerId, connections);
			}
			String providerUserId = connection.getKey().getProviderUserId();
			int connectionIndex = userIds.indexOf(providerUserId);
			connections.set(connectionIndex, connection);
		}
		return connectionsForUsers;
	}


	public Connection<?> getConnection(ConnectionKey connectionKey) {
		try {
			
			EntityManager em = this.getEntityManager();
			
			Query query = em.createQuery("SELECT o FROM grdnlog.bean.AppEngineConnectionData WHERE o.userId = ?1 AND o.providerId = ?2 AND o.providerUserId = ?3");
			query.setParameter(1, userId);
			query.setParameter(2, connectionKey.getProviderId());
			query.setParameter(3, connectionKey.getProviderUserId());
			
			AppEngineConnectionData data = (AppEngineConnectionData) query.getSingleResult();
			
			return (Connection<?>) mapConnectionData(data.getConnectionData());
		} catch (NoResultException e) {
			throw new NoSuchConnectionException(connectionKey);
		}
	}

	@SuppressWarnings("unchecked")
	public <A> Connection<A> getConnection(Class<A> apiType, String providerUserId) {
		String providerId = getProviderId(apiType);
		return (Connection<A>) getConnection(new ConnectionKey(providerId, providerUserId));
	}

	@SuppressWarnings("unchecked")
	public <A> Connection<A> getPrimaryConnection(Class<A> apiType) {
		String providerId = getProviderId(apiType);
		Connection<A> connection = (Connection<A>) findPrimaryConnection(providerId);
		if (connection == null) {
			throw new NotConnectedException(providerId);
		}
		return connection;
	}

	@SuppressWarnings("unchecked")
	public <A> Connection<A> findPrimaryConnection(Class<A> apiType) {
		String providerId = getProviderId(apiType);
		return (Connection<A>) findPrimaryConnection(providerId);
	}
	
	@Transactional
	public void addConnection(Connection<?> connection) {
		try {
			ConnectionData data = connection.createData();
			EntityManager em = this.getEntityManager();
			
			Query query = em.createQuery("SELECT o FROM grdnlog.bean.AppEngineConnectionData WHERE o.userId = ?1 AND o.providerId = ?2 ORDER BY o.rank DESC");
			query.setParameter(1, userId);
			query.setParameter(2, data.getProviderId());
			
			List<AppEngineConnectionData> queryResultList = query.getResultList();
			
			int rank = 1;
			
			if (queryResultList != null && !queryResultList.isEmpty()) {
				if (queryResultList.get(0).getRank() != null) {
					rank = queryResultList.get(0).getRank().intValue();
				}
			}
			
			AppEngineConnectionData connectionData = new AppEngineConnectionData();
			
			connectionData.setUserId(userId);
			connectionData.setProviderId(data.getProviderId());
			connectionData.setProviderUserId(data.getProviderUserId());
			connectionData.setRank(new Long(rank));
			connectionData.setDisplayName(data.getDisplayName());
			connectionData.setProfileUrl(data.getProfileUrl());
			connectionData.setImageUrl(data.getImageUrl());
			connectionData.setAccessToken(encrypt(data.getAccessToken()));
			connectionData.setSecret(encrypt(data.getSecret()));
			connectionData.setRefreshToken(encrypt(data.getRefreshToken()));
			connectionData.setExpireTime(data.getExpireTime());
			
			em.persist(connectionData);
		} catch (EntityExistsException e) {
			throw new DuplicateConnectionException(connection.getKey());
		}
	}
	
	public void updateConnection(Connection<?> connection) {
		ConnectionData data = connection.createData();
		
		EntityManager em = this.getEntityManager();
		
		String id = userId + "-" + data.getProviderId() + "-" + data.getProviderUserId();
		
		AppEngineConnectionData connectionData = em.find(AppEngineConnectionData.class, id);
		
		connectionData.setDisplayName(data.getDisplayName());
		connectionData.setProfileUrl(data.getProfileUrl());
		connectionData.setImageUrl(data.getImageUrl());
		connectionData.setAccessToken(encrypt(data.getAccessToken()));
		connectionData.setSecret(encrypt(data.getSecret()));
		connectionData.setRefreshToken(encrypt(data.getRefreshToken()));
		connectionData.setExpireTime(data.getExpireTime());
		
		em.merge(connectionData);
	}

	public void removeConnections(String providerId) {
		EntityManager em = this.getEntityManager();
		
		Query query = em.createQuery("DELETE FROM grdnlog.bean.AppEngineConnectionData WHERE o.userId = ?1 AND o.providerId = ?2");
		query.setParameter(1, userId);
		query.setParameter(2, providerId);
		
		query.executeUpdate();
	}

	public void removeConnection(ConnectionKey connectionKey) {
		EntityManager em = this.getEntityManager();
		
		Query query = em.createQuery("DELETE FROM grdnlog.bean.AppEngineConnectionData WHERE o.userId = ?1 AND o.providerId = ?2 AND o.providerUserId = ?3");
		query.setParameter(1, userId);
		query.setParameter(2, connectionKey.getProviderId());
		query.setParameter(3, connectionKey.getProviderUserId());
		
		query.executeUpdate();		
	}

	// internal helpers
	
	
	private Connection<?> findPrimaryConnection(String providerId) {
		EntityManager em = this.getEntityManager();
		
		Query query = em.createQuery("SELECT o FROM grdnlog.bean.AppEngineConnectionData WHERE o.userId = ?1 AND o.providerId = ?2 AND o.rank = 1");
		query.setParameter(1, userId);
		query.setParameter(2, providerId);
		
		List<AppEngineConnectionData> queryResultList = query.getResultList();
		if (queryResultList.size() > 0) {
			return mapConnectionData(queryResultList.get(0).getConnectionData());
		} else {
			return null;
		}	
	}
	
//	private final ServiceProviderConnectionMapper connectionMapper = new ServiceProviderConnectionMapper();
//	
//	private final class ServiceProviderConnectionMapper implements RowMapper<Connection<?>> {
//		
//		public Connection<?> mapRow(ResultSet rs, int rowNum) throws SQLException {
//			ConnectionData connectionData = mapConnectionData(rs);
//			ConnectionFactory<?> connectionFactory = connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId());
//			return connectionFactory.createConnection(connectionData);
//		}
//		
//		private ConnectionData mapConnectionData(ResultSet rs) throws SQLException {
//			return new ConnectionData(rs.getString("providerId"), rs.getString("providerUserId"), rs.getString("displayName"), rs.getString("profileUrl"), rs.getString("imageUrl"),
//					decrypt(rs.getString("accessToken")), decrypt(rs.getString("secret")), decrypt(rs.getString("refreshToken")), expireTime(rs.getLong("expireTime")));
//		}
//		
//		private String decrypt(String encryptedText) {
//			return encryptedText != null ? textEncryptor.decrypt(encryptedText) : encryptedText;
//		}
//		
//		private Long expireTime(long expireTime) {
//			return expireTime == 0 ? null : expireTime;
//		}
//		
//	}

	private <A> String getProviderId(Class<A> apiType) {
		return connectionFactoryLocator.getConnectionFactory(apiType).getProviderId();
	}
	
	private String encrypt(String text) {
		return text != null ? textEncryptor.encrypt(text) : text;
	}
	
	private EntityManager entityManager;

	@PersistenceContext
	public void setEntityManager(EntityManager entityManager) {
		this.entityManager = entityManager;
	}
	
	public EntityManager getEntityManager() {
		return entityManager;
	}

}
